昨天用 issue #427 到 PR #428 這個真實案例,講清楚「系統性盤點」怎麼幫功能開發收斂範圍。但一份 PR 描述裡的「同一分量級」「靜態標注」這些詞,具體落到程式碼裡是什麼樣子?今天要真的打開這份 PR,逐步走一次它的實作歷程——這篇比昨天更技術,聚焦在「怎麼做」而不是「該不該做」。
PR #428 的實際改動是 319 行新增、6 行刪除,涵蓋 9 個檔案。這個規模本身就呼應了昨天講的「同一分量級」——不是一個大重構,是在既有解析邏輯上,針對兩個新語法特性(todo(assignee:, issue:) 具名參數、skipOnCi()/skipLocally())做對稱的擴充。
改動的檔案分成兩組,對應這個擴充套件的兩層架構:
packages/phpunit/:真正的語法解析邏輯(TestExtractor.ts、TestExtractorForPest.test.ts、PestResolver.ts、types.ts),負責把原始碼解析成結構化的測試項目資訊packages/extension/:VS Code 端的呈現邏輯(TestHierarchyBuilder.ts、TestCollection.test.ts),負責把解析結果轉成 Test Explorer 看得懂的資料結構跟圖示這個分層本身就是一種架構邊界的體現——解析邏輯不需要知道 VS Code Test Explorer 長什麼樣,呈現邏輯不需要知道底層用哪個語法分析器,兩層各自獨立測試、獨立驗證。
skipOnCi()/skipLocally() 這兩個方法,實際會不會跳過測試,要看執行當下的環境變數(是不是在 CI 環境跑)。理論上有兩種偵測方式:等測試真的執行過、看 TeamCity 協定回報的結果來判斷;或者在解析原始碼的當下,就靜態標注「這個測試呼叫鏈裡有條件式跳過邏輯」。
PR 描述裡明講選擇了後者——靜態標注成 annotations.conditionalSkip: 'onCi' | 'locally',用一個獨立的圖示($(question),問號)跟「已經確定會跳過」的圖示($(circle-slash),圓圈斜線)區分開來。這個圖示選擇本身傳達了一個誠實的訊息:這裡標記的是「可能會跳過」,不是「一定會跳過」——因為靜態解析階段,程式碼還沒真的執行,沒辦法知道當下環境變數的值。執行期真正的跳過判斷,仍然走既有的 TeamCity testIgnored 路徑處理,這部分完全沒有改動。
用一組對照來看這個設計決策:
❌ 如果選擇「假裝能確定」:
把 skipOnCi() 的測試直接標成「一定會跳過」
→ 在本機開發環境(不是 CI)執行時,這個標注是錯的,
誤導使用者以為這個測試不會跑
✅ 實際採用的設計:
用獨立圖示標注「這裡有條件式跳過邏輯,但要看執行環境」,
真正的跳過判斷留給執行期的 TeamCity 回報
→ 靜態解析的角色是「提示可能性」,
不假裝自己知道執行期才會決定的事
昨天提到 issue #427 的盤點裡,每一條結論都「用兩套不同的語法分析器(tree-sitter 跟 php-parser)實際驗證過」。這個做法延續到了 PR #428 的實作裡——PestResolver.ts(對應其中一種解析路徑)跟 TestExtractor.ts(對應另一種)都各自加了對應的邏輯,兩邊的測試檔案(TestExtractorForPest.test.ts、interpret.test.ts)也都各自新增了測試案例。
為什麼要兩套語法分析器都驗證一次,而不是選一套信任到底? 因為這個專案要支援使用者環境裡可能存在、也可能不存在某個原生模組的情況——沒有這個模組時,退回另一套純 JavaScript 實作的解析器。如果只在一套分析器裡驗證過新語法的解析邏輯,另一套分析器沒有對應更新,會產生「在某些使用者的環境裡這個功能正常,在另一些環境裡完全沒反應」的落差,而且這種落差通常要等使用者回報才會被發現。雙路徑驗證,本質上是把「這個功能在所有支援環境下都一致」這件事,從查證變成了測試套件裡的具體斷言。
PR 的 Test plan 清單裡第一條寫著「TDD: red before green for all new cases」——每一個新案例都先確認測試會失敗,再寫實作讓它通過。這正好呼應我另外在寫的《AI 時代的 TDD:讓 AI 寫 Code,但不要讓它決定品質》系列反覆強調的紀律:AI 一次生成「實作+測試」時,測試從沒有真的紅過就直接綠了,等於沒人驗證過這個測試真的有偵測能力。 這份 PR 明確把「紅燈先於實作」列成檢查項,而不是事後才補測試,是把這條紀律變成可以被檢查的具體流程步驟,而不只是原則性的呼籲。
Test plan 裡另外列出兩個套件各自的測試套件都要跑過(分別是 1046 個跟 365 個測試案例)、型別檢查要乾淨、pre-commit 檢查要過——這些都是在合併前就能被自動驗證的具體項目,不是「看起來應該沒問題」的主觀判斷。
回想你參與過的功能開發:這個功能有沒有兩種(或以上)可能的偵測/實作方式?當時是怎麼決定選哪一種的——是因為對使用者更誠實(像今天這個案例),還是純粹因為比較好實作?
第二部到此告一段落。明天進入第三部:社群溝通——AI 能不能代替維護者回覆 issue?該不該?